/*! 
 * jquery.ux.docsBrowser - v 1.0.0 - http://jquery-ux.com/
 * Copyright (c) 2010 Michael Helgeson, Three Dub Media
 * Open Source MIT License - http://jquery-ux.com/license 
 */
// Created: 2009-12-08 
// Updated: 2010-07-28
// REQUIRES: jquery 1.4+, jquery.ux

;(function( $ ){ // confine scope

$.ux( "docsBrowser", {
	version: "1.0.0",
	expose: "", // + enable disable destroy
	defaults: {
		source: "", // xml file to load
		delay: 200, // ms between inspecting the hash
		toc: "td:last", // selector/expression for Table of Contents insertion
		body: "td:first", // selector/expression for body insertion
		lessTocText: "-", // table of contents toggle
		moreTocText: "+", // table of contents toggle
		prevText: "&laquo; Previous", // Previous Section Nav Link
		nextText: "Next &raquo;", // Next Section Nav Link
		breadcrumb: ' / ', // chars between breadcrumb links
		hashid: "", // the url hash prefix that makes the hash unique 
		hashsep: ".", // (NaN) the url hash digit delimiter
		toclist: "ul", // table of contents: <ul> or <ol>
		headline: "h2", // translate <content><h> 
		codetab: "   ", // replace "tabs" with "spaces" in <code>
		// the markup that creates the entire browser...
		markup: function(){
			return '<table cellpadding="0" cellspacing="0" class="'+ this.classify("wrap") +'">'
				+'<tr><td class="'+ this.classify("body") +'"></td>'
				+'<td class="'+ this.classify("toc") +'"></td></tr></table>';
			}
		// classifieds...
		// "wrap" // classname of markup container
		// "body" // classname of the content container
		// "toc" // classname of the table of contents container
		// "search" // classname of the search text input
		// "submit" // classname of the search submit button
		// "active" // classname of active TOC link
		// "toggle" // classname of the +/- toc links
		// "found" // classname of highlighted search terms
		// "description" // classname of paragraph holding section descriptions (and byline)
		// "demo" // classname of the div that wraps a demo iframe
		// "prev" // classname of "previous section" link
		// "next" // classname of "next section" link 
		// "b" // bold formatting
		// "i" // italic formatting
		// "u" // underline formatting
		},
	create: function( elem, opts ){ 
		// inherit default and custom settings
		var self = this;
		// inject the markup
		this.$container = $( elem ).html( opts.markup.call( this ) );
		this.$toc = this.$container.find( opts.toc );
		this.$body = this.$container.find( opts.body );
		// load the XML with AJAX
		$.ajax({
			url: opts.source,
			type: "GET",
			dataType: "xml",
			success: function( xml ){
				self.init.call( self, xml );
				}
			});
		},	
	disable: function( disabled ){
		if ( !disabled ) // enable
			this.start();
		},
	destroy: function(){
		clearTimeout( this.timer ); 
		//// $( window ).unbind("hashchange", $.proxy( this, "load") );
		this.$container.empty();
		},
	methods: {
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// initialize the Subject browser with real XML
		init: function( xml ){
			// initialize some internal properties
			this.lookup = {};
			this.cache = {};
			this.index = [];
			this.searchindex = [];
			this.links = [];
			this.$links = $([]);
			this.$active = $([]);
			this.searchPath = "search";
			this.indexPath = "index";
			// builds up hierarchical ids
			this.depth = this.options.hashid ? [ this.options.hashid ] : [];
			this.searchPath = this.makeID( this.searchPath );
			// store and inspect the argument
			this.xml = xml;
			this.$root = $("docs", xml );
			// store the <meta> data...
			this.parseMeta();
			// parse a <structure> hierarchy or parse each <section>
			this.parseSection( this.parseStructure() || this.$root );
			// inject the table of contents
			this.createTOC();
			// begin montioring the URL hash
			this.start();
			},
		start: function(){
			var self = this;
			this.timer = setTimeout(function(){
				if ( self.disabled ) return;			
				// try to load the current hash
				self.load.call( self, String( window.location ) );				
				// recurse...
				self.timer = setTimeout( arguments.callee, self.options.delay );
				}, 0 );	
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// load a section from the given url src
		load: function( src ){
			//// src = String( window.location );
			// parse the section id from the url hash
			var id = src.split("#")[1] || 0, 
			$found = $([]), top, self = this, rem;
			// skip if paused or no change
			if ( this.wait || this.last == id ) return;	
			// remember for next comparison
			this.last = id;
			// inject the new content
			this.$body.html( this.getSection( id ) );
			// optionally highlight search term
			if ( this.highlight ){
				// search ALL body nodes
				this.$body.find('*').contents().each(function(){
					// test text nodes for a substring match
					if ( rem = self.highlight.exec( this.nodeValue || "" ) ){
						var query = rem[0].toLowerCase(), len = query.length,
						node = this, index, temp;
						while ( -1 < ( index = node.nodeValue.toLowerCase().indexOf( query ) ) ){
							temp = node.splitText( index );
							node = temp.splitText( len );
							$( temp ).replaceWith( $('<span/>').addClass( self.classify("found") ).text( rem[0] ) );
							}
						}
					});
				// highlight all nodes that contain query string
				//$found.addClass( this.classify("found") );
				// find the Y coord of the first found node
				top = ( $found.offset() || {} ).top;
				}
			// scroll to the highlighted element or the top
			$( document ).scrollTop( top || 0 );
			},	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// read the xml meta section and store data in json
		parseMeta: function(){
			var $meta = this.$root.find("meta"), 
			output = [], self = this,
			meta = this.meta = {
				title: this.getText( $meta.find("title") ),
				summary: this.getText( $meta.find("summary") ),
				legal: this.getText( $meta.find("legal") ),
				updated: this.getText( $meta.find("updated") ),
				lang: this.getText( $meta.find("lang") ),
				keywords: [], webpages: [], authors: []
				};	
			// multiple keywords	
			$meta.find("data[name=keywords]").each(function( i, node ){
				[].push.apply( meta.keywords, self.getText( $( node ) ).split(',') );								
				});
			// multiple webpages	
			$meta.find("data[name=link]").each(function( i, node ){
				meta.webpages.push( self.getText( $( node ) ) );
				});
			// multiple authors
			$meta.find("author").each(function( i, node ){
				meta.authors.push( self.getText( $( node ) ) );								
				});
			// insert a search box...
			this.links.push('<form action="#', this.searchPath, '">',
				'<input type="text" class="', this.classify("search"), '" />',
				'<input type="submit" value="Search" class="', this.classify("submit"), '" />',
				'</form>');
			// insert the document topic
			// this.links.push('<p><a href="#">'+( this.meta.title )+'</a></p>');
			// create the meta page markup
			output.push('<span>&nbsp;</span><h1>', meta.title, '</h1>');
			// byline should not be omitted
			if ( meta.authors.length ) 
				output.push('<p class="', this.classify("description"), '">By ', meta.authors.join(', '), '</p>');
			// abstract summary may be omitted
			if ( meta.summary ) output.push('<p>', meta.summary, '</p>');
			// legal may be omitted
			if ( meta.legal ) output.push('<p>', meta.legal, '</p>');
			
			// optional external links
			if ( meta.webpages.length ){
				output.push('<p>');
				$.each( meta.webpages, function( i, src ){
					output.push('<a href="'+ src +'" class="ext">'+ src +'</a><br />');
					});
				output.push('</p>');
				}
			// updated may be omitted
			if ( meta.updated ) 
				output.push('<p>', meta.updated, '</p>');
			// insert a navigation link
			output.push('<br/><p><a href="#1" class="', this.classify('next'), '">Begin &raquo;</a>&nbsp;</p>&nbsp;');
			// insert the meta page into the cache	
			this.cache[ this.options.hashid ] = this.cache["0"] = output.join("");
			// store a reference to the meta element
			this.$meta = $meta;
			}, 
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~	
		// index the optional <structure> map
		parseStructure: function(){
			var $structure = this.$meta.find(' > sect');
			if ( $structure.length )
				return ( this.$structure = this.$meta );
		}, 	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~	
		// index each <section> or <sect>
		parseSection: function( $parent ){
			var self = this, $node, id,
			// determine the child selector
			child = self.$structure ? "sect" : "section",
			// find the <section> children
			$kids = $parent.find( ">"+ child );
			// make sure there are children
			if ( $kids.length ){
				// push the depth in
				self.changeDepth( +1 );
				// start a new list
				self.links.push('<',self.options.toclist,'>');
				// each <section>
				$kids.each(function( i, node ){
					var $orig = $( node );
					// cross ref <sect> to a true <section>
					$node = child == 'sect' ? self.$root.find('#'+ $orig.attr('ref') ) : $orig;
					// skip bad nodes
					if ( !$node.length ) 
						return;	
					// index the <section>
					id = self.storeIndex( $node );
					// add an <title> to the TOC
					self.links.push('<li>',( 
						$orig.is(':has('+ child +')') ? 
							'<span class="'+ self.classify("toggle")+ '">'+( self.options.moreTocText )+'</span>' : 
							'<span>&nbsp;</span>' 
						), 
						'<a href="#', id, '">', $.trim( self.getText( $node.find('>topic') ) ), '</a></li>'
					);
					// store and index search results
					self.storeSearch( id, $node );
					// recurse
					self.parseSection( $orig );
					});
				// close the current list
				self.links.push('</',self.options.toclist,'>');
				// pull the depth out
				self.changeDepth( -1 );
				}
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// store a node in the index, return a unique ID
		storeIndex: function( $node ){
			// increment the deepest section identifier
			this.depth[ this.depth.length - 1 ] += 1;
			// convert it to a string
			var id = this.makeID(), //this.depth.join( this.options.hashsep ), 
			// read any attr idref
			attr = $node.attr("id");
			// store the node reference
			this.lookup[ id ] = $node;
			// store the id for cross references
			this.lookup[ attr ] = id; //this.lookup['#'+ this.options.hashid + this.options.hashsep + attr ] = id;
			// store the id for indexing
			this.index.push( id );
			// return the id
			return id;
			},	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// make the index depth move in/out
		changeDepth: function( adj ){
			// add a new layer of depth
			if ( adj > 0 ) this.depth.push( 0 );
			// remove a layer of depth
			if ( adj < 0 ) this.depth.pop();
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// join the various parts of the ID
		makeID: function( add ){
			return ( add ? this.depth.concat( add ) : this.depth ).join( this.options.hashsep );
			},	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// search the index for the correct ID
		findIndex: function( id ){
			var i, more = this.options.moreTocText, self = this;
			// unhighlight current indexed item
			this.$active.removeClass( this.classify("active") );
			// search the index for a match
			for ( i = 0; i < this.index.length; i++ )
				if ( this.index[ i ] == id ) break;
			// highlight the new active item
			this.$active = this.$links.eq( i ).addClass( this.classify("active") );
			// walk up the parent nodes to make sure they are open
			this.$active.parents("ul").each(function(){
				$( this ).prev("li").find('span.'+ self.classify("toggle") ).trigger("click",true);
				});
			// make sure the active sub is showing
			this.$active.prev("span."+ this.classify("toggle") ).trigger("click",true);
			// return the matched number
			return i;
			},	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// make the prev/next section links
		makePrevNext: function( i ){
			// local refs
			var out = "", 
			next = this.index[ i + 1 ], 
			prev = this.index[ i - 1 ];
			// check for the previous link
			if ( this.lookup[ prev ] )
				out += '<a href="#'+ prev +'" class="'+ this.classify("prev") +'">'+ this.options.prevText +'</a>&nbsp;';
			else prev = false;			
			// check for the next link
			if ( this.lookup[ next ] )
				out += '<a href="#'+ next +'" class="'+ this.classify("next") +'">'+ this.options.nextText +'</a>&nbsp;';
			else next = false;
			// return the next id or false	
			return next || prev ? out : "";
			},		
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// return the table of contents DOM elements
		createTOC: function(){
			// add an index link...
			// this.links.push('<ul><li><span>&nbsp;</span><a href="#index">Index</a></li></ul>');
			// convert all the text to elements
			var $div = $( this.links.join('') ), self = this;
			// index all of the links
			this.$links = $div.eq(1).find("a");
			// index the search field
			this.$search = $div.find("input").eq( 0 );
			// bind the submit event
			$div.eq(0).bind("submit",function( ev ){
				window.location = '#'+ self.searchPath +'?'+ escape( self.$search.val() || "" );
				ev.preventDefault();
				});
			// bind the toggle +/- event
			$div.eq(1).bind("click",function( ev, forceOpen ){
				var $target = $( ev.target ), isClosed, 
				selector = "span."+ self.classify("toggle");
				// make links also toggle open/closed
				if ( $target.is("a") && $target.attr("href").split("#").pop() == self.last )
					$target.prev( selector ).click();
				// handle toggling the span	+/-
				else if ( $target.is( selector ) ){
					isClosed = forceOpen == undefined ? ( $target.text() == self.options.moreTocText ) : !!forceOpen;
					$target.closest("li").next("ul")[ isClosed ? "show" : "hide" ]();
					$target.text( isClosed ? self.options.lessTocText : self.options.moreTocText );
					}
				});			
			// insert the whole chunk
			this.$toc.append( $div );
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// return the identified section in HTML
		getSection: function( id ){ 
			// load the title page by default
			if ( !id ) id = "0";
			// check for index page
			if ( !id.indexOf( this.indexPath ) ) 
				return this.getIndex();
			// check for search and results
			if ( !id.indexOf( this.searchPath ) ) 
				return this.search();
			// check for search highlighting
			if ( id.indexOf('?') > -1 && this.getQuery() )
				// remember to highlight
				this.highlight = new RegExp( this.query.replace(/(\/|\.|\*|\+|\?|\$|\^|\||\(|\)|\[|\]|\{|\}|\\)/g, "\\$1" ), "i" );
			// highlight nothing	
			else this.highlight = null; 	
			// clean-up requested resource id
			id = id.split("?").shift();			
			// try to load the correct node
			var $node = this.lookup[ id ], txt = [], links, n;
			// check for element IDs too (double lookup redirect)
			if ( typeof $node == "string" || ( !$node && ( $node = this.lookup[ id.split('.').pop() ] ) ) ){
				this.wait = true; // pause reloading any new sections
				this.last = null; // force the page to re-display
				window.history.go( -1 ); // overwrites the elementID entry in browser history
				window.location = '#'+ $node +( this.query ? '?'+escape( this.query ) : "" ); // new page
				this.wait = false; // resume loading
				return;// break
				}
			// clear current search
			this.query = null;
			// finding the index also moves the highlight in the TOC
			n = this.findIndex( id );
			// check to see if it is already cached
			if ( this.cache[ id ] ){
				this.publish("change");	
				return this.cache[ id ];
				}
			// build up the breadcrumbs
			txt.push( this.makeBreadcrumbs( id ) );
			// throw an error if node is not found
			if ( !$node ) {
				txt.push('<span>&nbsp;</span><h1>Section not found</h1>',
					'<p>The section you have attempted to retrieve cannot be located.</p>');
				this.publish("lost");
				return txt.join(''); // not cached
				}
			// find the title
			txt.push('<h1>', this.getText( $node.find(' > topic') ), '</h1>');
			// find the summary
			txt.push('<p class="', this.classify("description"), '">', this.getText( $node.find(' > description') ), '</p>');
			// find the content
			txt.push( this.translate( $node.find(' > content') ) );
			// create the "prev/next" section links
			if ( links = this.makePrevNext( n ) ) 
				txt.push('<br/><p>', links, '</p>&nbsp;');
			this.publish("change");	
			// concatenate, and cache the result
			return ( this.cache[ id ] = txt.join('') );
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// treat example <code> specially, to preserve and offset white space
		prepCode: function( $code ){
			// skip missing elements
			if ( !$code.length ) return;
			// if it is a block level element, fix white space
			var output = [], temp, leader;
			// extract the text and break into lines
			$.each( this.translate( $code ).split('\n'), function( i, line ){
				if ( !leader && ( temp = (/^(\s*)\S/).exec( line ) ) )
					leader = new RegExp( "^"+ temp[1] );	
				output.push( leader ? line.replace( leader, "" ) : line );
				});
			return '<code>'+ $.trim( output.join('\n') ).replace(/\t/g, this.options.codetab ) +'</code>';
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// treat example <demo> specially, to allow display and linking
		prepDemo: function( $demo ){
			var uri = $demo.attr('src'), output = [];
			output.push('<div class="', this.classify("demo"), '">')
			output.push('<iframe frameborder="0" src="', uri, '"></iframe>');
			output.push('<a href="', uri, '" target="_blank">Open in a new window</a>');
			output.push('</div>');
			return output.join("");
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// translate lite <description> markup to HTML
		translate: function( $node ){
			var output = [], self = this, $child, tags, tag, url, $temp;
			$node.contents().each(function( i, child ){
				$child = $( child ); tags = [];
				// element nodes...
				if ( child.nodeType == 1 ){
					switch ( tag = ( child.nodeName || "" ).toLowerCase() ){
						case 'code':
							return output.push( self.prepCode( $child ) );
						case 'media':
							switch ( $child.attr('type') ){
								case 'html'://case 'demo':
									return output.push( self.prepDemo( $child ) );
								case 'image': //case 'image':
									return output.push('<img src="'+ $child.attr('src') +'" />');
							}
						// excerpt
						case 'x':
							$temp = self.$root.find('#'+ $child.attr("ref") +'>'+( $child.text() || "topic" ) );
							if ( !$temp.length ) 
								$temp = self.$root.find('#'+ $child.attr("ref") +'>data[name='+ $child.text() +']');
							return output.push( self.translate( $temp ) ); 	
						// links
						case 'a':
							url = $child.attr("href");
							// cross reference
							if ( !url.indexOf('#') ){
								tags = ['<a href="#'+ self.lookup[ url.substr(1) ] +'">','</a>']; 
								if ( !$child.contents().length ) // tag is empty, use reference section > title
									return output.push( tags[0], self.getText( self.$root.find( url +' > topic') ), tags[1] );
								}
							// normal anchor	
							else {	
								tags = ['<a href="'+ url +'">','</a>']; 
								if ( !$child.contents().length ) // tag is empty, use href value
									return output.push( tags[0], $child.attr("href"), tags[1] );
								}
							break;
						// healine	
						case 'h':
							tags = ['<'+ self.options.headline +'>','</'+ self.options.headline +'>'];
							break;
						// line breaks	
						case 'br':
							return output.push('<br />');
						// formatting
						case 'b': // bold
						case 'i': // italic
						case 'u': // underline
							tags = ['<span class="'+ self.classify( tag ) +'">','</span>']; 
							break;
						case 'em': // emphasis
							tags = [
								'<span class="'+ 
								$.map( ( $child.attr("style") || "italic" ).split(" "),function( str ){
									return self.classify( str );
								}).join(" ") 
								+'">',
								'</span>'
							]; 
							break;
						case 'li':
						case 'p':
						case 'ul':
						case 'table': 
						case 'tr': 
						case 'th': 
						case 'td':
						default:
							tags = ['<'+ tag +'>','</'+ tag +'>'];
							break;
						// end swicth
						}
					// recurse child nodes
					output.push( tags[0], self.translate( $child ), tags[1] );
					}
				// assume plain old text node, not comments
				else if ( child.nodeType != 8 ) 
					output.push( self.getText( child ) );
				});
			// concatenate output strings
			return output.join('');
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// regular expression that matches YYYY-MM-DD dates	
		regExpDate: (/(?:^|\s)(\d{4})-(\d{2})-(\d{2})(?:$|\s)/),
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// convert YMD date format to long english string
		formatDate: function( ymd ){
			var parts = this.regExpDate.exec( ymd ), 
			//months = (" Jan. Feb. Mar. Apr. May Jun. Jul. Aug. Sep. Oct. Nov. Dec.").split(" ");
			months = (" January February March April May June July August September October November December").split(" ");
			// can't parse, giveback the input
			if ( !parts ) return ymd;
			// 1/1/2010
			/*return '<span class="'+( this.classify("date") )+'">'
				+( parseInt( parts[2], 10 ) )+"/"
				+( parseInt( parts[3], 10 ) )+"/"
				+( parts[1] )
				+'</span>';*/
			// January 1, 2010
			return '<span class="'+( this.classify("date") )+'">'
				+( months[ parseInt( parts[2], 10 ) ] )+" "
				+( parseInt( parts[3], 10 ) )+", "
				+( parts[1] )
				+'</span>';
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// convert an id into a link trail of ancestors
		makeBreadcrumbs: function( id ){
			// split into each part
			var nums = id.split( this.options.hashsep ), i, oid, $node, 
			out = ['<a href="#'+ this.options.hashid +'">'+( this.meta.title )+'</a>'];
			// go through each part
			for ( i = 1; i < nums.length; i++ ){
				// extract the id of the ancestor
				oid = nums.slice( 0, i ).join( this.options.hashsep );
				// lookup the ancestor in the cache
				$node = this.lookup[ oid ];
				// skip ids that are not found
				if ( !$node || !$node.length ) continue;
				// create a link
				out.push('<a href="#'+ oid +'">'+ this.getText( $node.find(' > topic') ) +'</a>');
				}
			// concatenate the links
			return ( out.length ? out : ["&nbsp;"] ).join( this.options.breadcrumb ) ;
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// store a node and id in an index to be searched
		storeSearch: function( id, $node ){
			// cache the index and text, leave space for result
			this.searchindex.push({
				id: id, result: false,
				text: $.map( $node.children().not("section"), function( el ){
					return $( el ).text().toLowerCase();
					}).join(" ")
				});
			},	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// read the query from the hash
		getQuery: function( q ){
			var query = q || unescape( String( window.location ).split('#').pop().split('?').pop() );
			this.$search.val( query );
			return !!( this.query = query );
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// create and/or display the keyword index page
		getIndex: function(){
			// create the index page
			if ( !this.indexpage ){
				var self = this, out = [ this.makeBreadcrumbs('#'), '<h1>Index</h1>' ], 
				terms, words = [], counts = {}, lookup = {};
				// find all the keyword data
				this.$root.find('section > data[name=keywords]').each(function(){
					// split up comma seperated values
					$.each( ( self.getText( this ) || "" ).split(','), function( i, str ){
						// remove leading/trailing spaces
						str = $.trim( str );
						lower = str.toLowerCase();
						// increment existing words
						if ( counts[ lower ] )
							counts[ lower ] += 1;
						// add a new word	
						else {
							counts[ lower ] = 1;
							lookup[ lower ] = str;
							words.push( lower );	
						}	
					});
				});
				// sort and iterate to build the final markup...
				$.each( words.sort(), function( i, word ){
					out.push('<a href="#', self.searchPath ,'?', word ,'">', lookup[ word ] ,'</a><br/>');
				});
				this.indexpage = out.join('') + '<br/>&nbsp;';
			}
			// display the index page
			this.highlight = null;
			this.$body.html( this.indexpage );
			this.findIndex();
		},	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// execute a search
		search: function( query ){
			this.getQuery( query );
			this.highlight = null;
			// begin the output
			var test = $.trim( this.query.toLowerCase() ), single, self = this,
			out = [ this.makeBreadcrumbs('#'), '<h1>Search Results...</h1>' ];
			// filter and sort the results
			arr = $.map( this.searchindex, function( page ){
				page.score = page.text.indexOf( test );			
				return page.score > -1 ? page : null;
				})/*.sort(function(a,b){ 
					return a.score - b.score; 
					});*/
			// convert results to insertable elements
			arr = $.map( arr, function( page ){ 
				single = page.id; // to jump to a single result
				// if the result does not exist, create it
				return self.makeSearchResult( page.id );
				});
			// continue the output
			if ( !arr.length ) out.push('<p>No matches found</p>');
			else out.push('<p>There are '+( arr.length )+' items that match ',
				'<span class="', this.classify("found"), '">', this.escapeTags( this.query ), '</span></p>');
			// redirect to a single matched result...
			if ( arr.length == 1 ){
				this.last = "null"; // force refresh
				window.location = '#'+ single +'?'+ escape( this.query );
				}
			// or append and inject the results
			else this.$body.html( out.concat( arr ).join('') );
			this.findIndex();
			},	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// execute a search
		makeSearchResult: function( id ){
			var $node = this.lookup[ id ];
			if ( !$node ) return "";
			return '<p><a href="#'+( id )+'?'+ escape( this.query ) +'">'+ this.getText( $node.find(' > topic') ) +'</a><br />'
				+ this.getText( $node.find(' > description') ) +'</p>';
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// pull the text value safely out of a node, format dates
		getText: function( node ){
			var self = this;
			return this.escapeTags( ( $( node ).contents()[0] || node ).nodeValue )
				.replace( this.regExpDate, function( str ){ return self.formatDate( str ); });
			},
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		// replace "<" and ">" with html entities
		escapeTags: function( str ){
			return ( str || "" ).replace(/</g,'&lt;').replace(/>/g,'&gt;');
			}	
		//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
		}
	});

})( jQuery ); // secure the $ jQuery alias